Skip to content

Multi partition cagra search#2035

Open
jamxia155 wants to merge 70 commits into
NVIDIA:mainfrom
jamxia155:multi-segment-cagra-search
Open

Multi partition cagra search#2035
jamxia155 wants to merge 70 commits into
NVIDIA:mainfrom
jamxia155:multi-segment-cagra-search

Conversation

@jamxia155

@jamxia155 jamxia155 commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

This PR adds a CAGRA search API and associated implementation for batching workloads involving a single query and multiple indexes (e.g. index segments built from one dataset by cuvs-lucene). This promotes GPU utilization for non-batched queries by parallelizing work over multiple CTAs, while minimizing the number of concurrent kernels for potential host-side parallelism.

Addresses cuvs-lucene issue 124, issue 158, issue 159, and cuvs issue 2166.

Associated cuvs-lucene PR: NVIDIA/cuvs-lucene#133.

jamxia155 and others added 8 commits April 22, 2026 12:58
  Introduces a globally-optimized path for searching across multiple CAGRA
  index segments without per-segment device-to-host copies.

  - c/include/cuvs/selection/select_k.h, c/src/selection/select_k.cpp:
    new C API function cuvsSelectK that wraps cuvs::selection::select_k
    for selecting the k smallest float values from a flat device array

  - BufferedCagraSearch (internal interface): searchIntoBuffer() writes
    per-segment CAGRA results into a slice of a caller-owned device buffer
    without syncing the stream or copying to host

  - CagraIndexImpl: implements BufferedCagraSearch; searchIntoBuffer()
    computes byte offsets into the global buffer using segmentIdx * topK

  - SelectKHelper: Panama FFI binding for cuvsSelectK

  - MultiSegmentCagraSearch: orchestrates the full pipeline — queue all
    per-segment searches, sync once, run cuvsSelectK on GPU, sync again,
    single D2H copy, decode results

  - MultiSegmentSearchResults: simple result carrier with count,
    segmentIndices, ordinals, and distances arrays
  Implements concurrent per-segment GPU search that eliminates per-segment
  device-to-host copies and CPU blocking on workspace deallocation.
  Key changes:
  - CudaStreamPool: fixed-size pool of non-blocking CUDA streams (one
    cuvsResources_t per slot). Segments are assigned to slots via
    round-robin so searches on different slots run concurrently. Pool size
    defaults to 8 and is overridden via the system property
    com.nvidia.cuvs.streamPoolSize.
  - MultiSegmentCagraSearch: single-query search across N index segments
    using the stream pool. All per-segment CAGRA kernels write into a
    shared device buffer (no per-segment D2H copy or stream sync). A
    single cuvsSelectK call finds the global top-k entirely on GPU, then
    one D2H copy transfers the results.
  - BufferedCagraSearch / CagraIndexImpl: new searchIntoBuffer() method
    that queues a CAGRA search kernel on a caller-supplied stream and
    writes results at a given row offset into a pre-allocated device
    buffer.
  - cuvsRMMAsyncMemoryResourceEnable() (C API + Java bindings): switches
    the current device memory resource to cuda_async_memory_resource so
    that workspace deallocations issued by CAGRA's search plan destructor
    are stream-ordered and non-blocking. Without this, cudaFree serializes
    kernel launches across streams regardless of stream assignment,
    nullifying the stream pool benefit.
  CudaStreamPool was a static singleton shared across all threads. With
  multiple concurrent query threads, calls to MultiSegmentCagraSearch.search()
  would alias onto the same pool slots after Math.floorMod, causing concurrent
  cudaEventRecord and cudaStreamWaitEvent calls on the same event handle
  (undefined behavior) and cross-thread stream interference. Additionally,
  CudaStreamPool.closeInstance() was called from CuVSResourcesImpl.close(),
  so whichever thread closed its resources first would destroy the shared pool
  while other threads were still using it.

  Fix: make CudaStreamPool a per-CuVSResources instance rather than a static
  singleton. One pool is created and owned by each CuVSResourcesImpl; it is
  closed when that instance is closed. Since CuVSResources is thread-local in
  the Lucene integration, each query thread gets its own independent set of
  streams and events with no sharing or locking required.

  - CudaStreamPool: remove static singleton (getOrCreate, closeInstance, static
    volatile instance, static AtomicInteger slotCounter); add package-private
    constructor; replace static slotCounter with an instance int and a new
    nextSlot(int count) method.

  - CuVSResourcesImpl: add final CudaStreamPool streamPool field (sized from
    com.nvidia.cuvs.streamPoolSize system property); close it directly in
    close(); add static getStreamPool(CuVSResources) helper for
    MultiSegmentCagraSearch to retrieve the per-resources pool.

  - MultiSegmentCagraSearch: get pool via CuVSResourcesImpl.getStreamPool
    and advance via pool.nextSlot. Remove redundant cuvsStreamSync after
    cuvsSelectK — the D2H copies are enqueued on the same stream so CUDA
    ordering already serializes them. Replace three separate hostArena.allocate
    calls with one contiguous allocation (Long.BYTES-aligned) sliced into three
    typed views, reducing OS-level allocation overhead per query.
The persistent kernel runner was previously keyed on (dataset_desc,
graph, fixed search params), which forced a destroy/recreate cycle for
every segment when searching a multi-segment Lucene index: each segment
has a different graph pointer and potentially a different auto-computed
max_iterations, producing a different hash on every call.

C++ changes (search_single_cta_kernel-inl.cuh):
- Move dataset_desc_ptr, graph_ptr, and graph_degree from fixed runner
  state into per-job fields in job_desc_t. The persistent kernel reads
  them from the job descriptor, so one runner instance can serve any
  number of segments without being rebuilt.
- Remove dataset_desc and graph arguments from persistent_runner_t
  constructor and calculate_parameter_hash; the runner is now keyed
  only on fixed kernel parameters (block_size, smem_size, itopk, etc.).
- Update select_and_run to initialize the device descriptor on the
  caller's stream and synchronize before submission, then pass
  dd_dev_ptr, graph.data_handle(), and graph_degree to runner::launch.
- Remove dd_host from persistent_runner_t; dataset upload is now the
  caller's responsibility on each launch.

Java changes:
- Add persistent, persistentLifetime, and persistentDeviceUsage fields,
  getters, and Builder methods to CagraSearchParams.
- Wire the three persistent params through CuVSParamsHelper into the
  Panama-generated cuvsCagraSearchParams struct.
…raSearch

In persistent mode, `searchIntoBuffer` blocks on the CPU until the GPU
signals completion via a system-scope atomic. Previously, segments were
searched sequentially, so the GPU processed one segment at a time per
query, leaving its job queue mostly idle between segment dispatches.

Submit one async task per pool slot so all slots' segment searches are
in-flight simultaneously. The persistent runner's job queue can hold
all N segment jobs at once, allowing GPU workers to execute segments in
parallel (bounded by worker_queue_size).

Segments are grouped by pool slot rather than submitted one-per-segment
to prevent concurrent access to the same cuvsResources_t handle: the
descriptor_cache stored inside the RAFT resources object is not
thread-safe, and multiple threads calling cuvsCagraSearch with the same
handle causes a SIGSEGV. Grouping ensures each cuvsResources_t is
accessed by at most one thread at a time. Effective parallelism is
min(numSegments, pool.size()); increasing cuvsStreamPoolSize raises
the ceiling.

In non-persistent mode the existing sequential loop is unchanged:
kernel launches are asynchronous and return immediately, so Java-level
parallelism adds overhead without benefit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Multi-segment search (C++/C/Java):
- Add search_multi_segment() C++ function: builds a single search plan
  sized for the largest segment, packs per-segment descriptors, and
  launches search_kernel_ms — a new SINGLE_CTA kernel with grid
  dimensions (1, num_queries, num_segments) so each CTA independently
  searches one (query, segment) pair in a single kernel call.
- Add cuvsCagraSearchMultiSegment() C API wrapping the above.
- Simplify MultiSegmentCagraSearch.java to call
  cuvsCagraSearchMultiSegment() unconditionally, removing the previous
  per-segment stream pool, thread pool, CUDA event synchronization, and
  persistent/non-persistent branching. The search now completes in four
  phases: multi-segment kernel, GPU-side select-k, single D2H copy,
  result decoding.
- Add BufferedCagraSearch.getIndexHandle() to expose the raw
  cuvsCagraIndex_t handle needed by the multi-segment kernel dispatch.

Workspace pool (C/Java):
- Add cuvsResourcesSetWorkspacePool(): configures the per-resources
  temporary workspace as an uncapped RMM pool that grows without
  shrinking. After warmup, cuvsRMMAlloc/cuvsRMMFree hit the pool cache
  instead of calling cudaMallocAsync/cudaFreeAsync, eliminating CUDA
  context lock contention under concurrent query threads.
- Route cuvsRMMAlloc/cuvsRMMFree through the workspace resource so Java-
  side output buffer allocations also benefit from the pool.
- Expose setWorkspacePool() in the CuVSResources Java interface with
  implementations in CuVSResourcesImpl and SynchronizedCuVSResources.

Refactoring (search_single_cta_kernel-inl.cuh):
- Extract TopkVariant enum and select_topk_variant() helper, shared by
  search_kernel_config and search_kernel_config_ms, replacing duplicated
  if/else trees in both choose_itopk_and_mx_candidates() bodies.
- Extract kernel_dispatch_params::compute() to centralize max_candidates
  and max_itopk computation shared by select_and_run and
  select_and_run_multi_segment.
- Extract hashmap_element_count() static helper on the search struct,
  used by both set_params (single-segment) and run_multi_segment.
Add HALF(2) to the DataType enum and wire it through the Java layer so
callers can build and exchange float16 matrices without any Java-side
type conversion:

- CuVSMatrix.Builder: new addVector(short[]) overload; each element is
  a raw IEEE 754 binary16 bit pattern held in a short
- LinkerHelper: add C_SHORT via canonicalLayouts("short")
- CuVSMatrixBaseImpl: map HALF → C_SHORT in valueLayoutFromType();
  decode DLPack (kDLFloat, bits=16) → DataType.HALF in
  dataTypeFromTensor()
- CuVSMatrixInternal: HALF maps to the same kDLFloat DLPack type code
  as FLOAT; the bits field (16 vs 32) distinguishes them on the C side
- JDKProvider.MatrixBuilder: implement addVector(short[]) following the
  same MemorySegment.ofArray pattern as the other overloads
@jamxia155 jamxia155 requested review from a team as code owners April 22, 2026 23:02
@jamxia155 jamxia155 marked this pull request as draft April 22, 2026 23:15
@copy-pr-bot

copy-pr-bot Bot commented Apr 22, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Apr 22, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request introduces multi-segment CAGRA search functionality, GPU-based selection, and RMM memory pool management. It adds C and C++ APIs for multi-segment approximate nearest neighbor search, async memory allocation, and select-K operations, alongside corresponding Java bindings and internal implementations supporting concurrent per-segment GPU processing and stream-based result aggregation.

Changes

Cohort / File(s) Summary
Build Configuration
c/CMakeLists.txt
Added src/selection/select_k.cpp to the C API library source list.
C API: RMM Memory Management
c/include/cuvs/core/c_api.h, c/src/core/c_api.cpp
Introduced cuvsResourcesSetWorkspacePool to configure per-resources uncapped device memory pools and cuvsRMMAsyncMemoryResourceEnable to switch RMM to stream-ordered async allocation. Extended cuvsRMMMemoryResourceReset to clear thread-local async resources.
C API: Multi-Segment CAGRA
c/include/cuvs/neighbors/cagra.h, c/src/neighbors/cagra.cpp
Added cuvsCagraSearchMultiSegment to perform concurrent ANN searches across multiple CAGRA index segments, accepting per-segment index pointers and query/result tensors.
C API: Select-K
c/include/cuvs/selection/select_k.h, c/src/selection/select_k.cpp
Introduced cuvsSelectK to select k smallest values from GPU tensors, returning values and column indices.
C++ API: Multi-Segment CAGRA
cpp/include/cuvs/neighbors/cagra.hpp, cpp/src/neighbors/cagra.cuh, cpp/src/neighbors/cagra_search_inst.cu.in
Added public search_multi_segment function overloads for float, half, int8_t, uint8_t index types with configurable output neighbor types (uint32_t or int64_t). Included explicit template instantiations for multi-segment variants.
C++ Implementation: Multi-Segment Search Logic
cpp/src/neighbors/detail/cagra/cagra_search.cuh
Implemented multi-segment CAGRA search orchestration, handling dataset validation, kernel configuration, per-segment device descriptor management, and distance postprocessing (cosine-expanded normalization).
C++ Implementation: Multi-Segment Kernel Infrastructure
cpp/src/neighbors/detail/cagra/search_single_cta_kernel.cuh, cpp/src/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh, cpp/src/neighbors/detail/cagra/search_single_cta_inst.cuh
Added multi-segment kernel execution path with per-segment descriptor structure, concurrent CTA scheduling via blockIdx.z, hashmap sizing across segments, persistent runner refactoring to use job-specific descriptors, and select_and_run_multi_segment launcher.
C++ Implementation: Multi-Segment Search Helpers
cpp/src/neighbors/detail/cagra/search_single_cta.cuh
Introduced run_multi_segment and hashmap_element_count helpers to orchestrate concurrent segment processing and allocate global hashmap buffers.
Java: Minor Updates
java/cuvs-java/src/main/java/com/nvidia/cuvs/{CagraIndex,HnswIndex}.java, java/cuvs-java/src/main/java/com/nvidia/cuvs/{CuVSAceParams,HnswAceParams,HnswIndexParams}.java
Updated SPDX copyright headers and reformatted method signatures for consistency.
Java: Core API Extensions
java/cuvs-java/src/main/java/com/nvidia/cuvs/{CagraSearchParams,CuVSMatrix,CuVSResources,SynchronizedCuVSResources}.java
Added persistent kernel configuration options to CagraSearchParams, HALF data type support to CuVSMatrix, and setWorkspacePool method to CuVSResources interface and implementations.
Java: SPI
java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/{CuVSProvider,UnsupportedProvider}.java
Added enableRMMAsyncMemory() method to provider interface and stub implementation.
Java: Multi-Segment Search API
java/cuvs-java/src/main/java22/com/nvidia/cuvs/{MultiSegmentSearchResults,MultiSegmentCagraSearch}.java
Introduced result class to represent decoded multi-segment search outputs and public API for multi-segment search with global top-k selection via GPU-resident select-K.
Java: Buffered Search Interface
java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/{BufferedCagraSearch,CagraIndexImpl}.java
Defined interface for GPU-side buffered search into caller-provided device buffers and implemented support in CagraIndexImpl for per-segment search without host synchronization.
Java: Internal Helpers & Bindings
java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/{SelectKHelper,CuVSParamsHelper,CudaStreamPool,LinkerHelper}.java
Added native FFM bindings for select-K, CAGRA search parameters builder, fixed-size CUDA stream/resource pool with round-robin slot allocation, and C_SHORT layout constant.
Java: Internal Matrix & Resources
java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/{CuVSMatrixBaseImpl,CuVSMatrixInternal,CuVSResourcesImpl,HnswIndexImpl}.java
Added half-precision float tensor handling, integrated stream pool ownership in resources, workspace pool implementation via native API, and formatting adjustments.
Java: Provider Implementation
java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java
Implemented enableRMMAsyncMemory() and added short[] vector support for matrix building.
Java: Test Utilities
java/cuvs-java/src/test/java/com/nvidia/cuvs/CheckedCuVSResources.java
Implemented setWorkspacePool delegation in test wrapper.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ⚠️ Warning The PR title 'Multi partition cagra search' does not accurately represent the main change. The changeset implements multi-segment (not multi-partition) CAGRA search across the full stack (C, C++, Java), with new APIs (cuvsSelectK, BufferedCagraSearch), memory management features, and kernel optimizations. Update the title to 'Add multi-segment CAGRA search with select-k and async memory support' or similar to accurately reflect the scope and main components of the changeset.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description is directly related to the changeset, explaining the core feature (CAGRA multi-segment search API) and its purpose (batching workloads for single query, multiple indexes).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraSearchParams.java (1)

293-322: ⚠️ Potential issue | 🟡 Minor

The toString() method does not include the new persistent fields.

The three new fields (persistent, persistentLifetime, persistentDeviceUsage) are not included in the toString() output. While not critical, this creates an inconsistency where debugging output won't show the full configuration.

🔧 Proposed fix to include persistent fields in toString()
         + ", randXORMask="
         + randXORMask
+        + ", persistent="
+        + persistent
+        + ", persistentLifetime="
+        + persistentLifetime
+        + ", persistentDeviceUsage="
+        + persistentDeviceUsage
         + "]";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraSearchParams.java` around
lines 293 - 322, The toString() in class CagraSearchParams currently omits the
new fields persistent, persistentLifetime, and persistentDeviceUsage; update the
CagraSearchParams.toString() method to append these three fields (with their
names and values) into the returned string (same formatting style as the other
fields) so debugging output shows the full configuration.
🧹 Nitpick comments (3)
java/cuvs-java/src/test/java/com/nvidia/cuvs/CheckedCuVSResources.java (1)

59-62: Add destroyed-state guard in the new delegate method.

setWorkspacePool(...) should call checkNotDestroyed() before delegating, consistent with this wrapper’s defensive contract.

Proposed change
   `@Override`
   public void setWorkspacePool(long sizeBytes) {
+    checkNotDestroyed();
     inner.setWorkspacePool(sizeBytes);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@java/cuvs-java/src/test/java/com/nvidia/cuvs/CheckedCuVSResources.java`
around lines 59 - 62, The new delegate method setWorkspacePool(long sizeBytes)
is missing the wrapper's destroyed-state guard; modify
CheckedCuVSResources.setWorkspacePool to call checkNotDestroyed() at the start
(before delegating to inner.setWorkspacePool(sizeBytes)) so it matches the
class's defensive contract and uses the same destroyed-state check as other
methods.
java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraSearchParams.java (1)

527-537: Consider adding validation for persistentDeviceUsage bounds.

The Javadoc states the value "must be greater than 0.0 and not greater than 1.0", but the setter doesn't validate this constraint. Invalid values would only fail at the native layer.

🛡️ Optional: Add validation in the builder
     public Builder withPersistentDeviceUsage(float persistentDeviceUsage) {
+      if (persistentDeviceUsage <= 0.0f || persistentDeviceUsage > 1.0f) {
+        throw new IllegalArgumentException(
+            "persistentDeviceUsage must be > 0.0 and <= 1.0, got: " + persistentDeviceUsage);
+      }
       this.persistentDeviceUsage = persistentDeviceUsage;
       return this;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraSearchParams.java` around
lines 527 - 537, The builder setter withPersistentDeviceUsage currently assigns
persistentDeviceUsage without enforcing the Javadoc constraint; update
Builder.withPersistentDeviceUsage to validate that persistentDeviceUsage > 0.0f
and <= 1.0f and throw an IllegalArgumentException (including the invalid value
in the message) when the check fails so invalid values are rejected early before
reaching native code.
cpp/src/neighbors/detail/cagra/cagra_search.cuh (1)

388-417: Consider hoisting query_norms allocation outside the loop for CosineExpanded metric.

When multiple segments use CosineExpanded metric, query_norms is allocated and computed redundantly for each segment. Since queries are the same across segments (repeated query vector), the norms could be computed once.

This is a minor optimization opportunity. The current implementation is correct; the redundant computation is bounded by the number of segments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cpp/src/neighbors/detail/cagra/cagra_search.cuh` around lines 388 - 417,
Hoist the allocation and computation of query_norms out of the segment loop and
reuse it for every segment whose indices[i]->metric() ==
cuvs::distance::DistanceType::CosineExpanded: allocate query_norms once (using
raft::make_device_vector) before the for-loop, run raft::linalg::reduce on the
shared queries data to compute the norms, then inside the loop call
raft::linalg::matrix_vector_op (as currently done) using the precomputed
query_norms for each CosineExpanded segment; after the loop free or let
query_norms go out of scope. Ensure you still call
cuvs::neighbors::ivf::detail::postprocess_distances for non-CosineExpanded
branches and keep all existing ops (raft::compose_op, raft::sq_op,
raft::div_const_op, raft::cast_op, raft::add_const_op, raft::div_checkzero_op)
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@c/include/cuvs/core/c_api.h`:
- Around line 229-240: The current implementation of
cuvsRMMAsyncMemoryResourceEnable stores cuda_async_memory_resource in a
thread_local async_mr and passes it to rmm::mr::set_current_device_resource(),
creating a lifetime mismatch vs the API doc that says the change is global;
change the implementation so the async memory resource is process-scoped (not
thread_local) by replacing thread_local async_mr with a static/process-global
instance (e.g., a static unique_ptr or static object) so it outlives the thread
and remains valid for rmm::mr::set_current_device_resource(), and ensure
cuvsRMMAsyncMemoryResourceEnable and any cleanup use that same process-global
symbol (cuda_async_memory_resource / async_mr) when setting or resetting the
current device resource; alternatively, if thread-local semantics are intended,
update the cuvsRMMAsyncMemoryResourceEnable documentation to state it only
affects the calling thread and keep async_mr thread_local.

In `@c/src/neighbors/cagra.cpp`:
- Around line 710-733: The loop currently casts every indices[i]->addr and
builds device views without validating each segment's types/devices; update the
loop that fills idx_vec, q_vec, n_vec, d_vec to perform the same per-segment
checks used in cuvsCagraSearch: assert indices[i] != nullptr and addr != 0
(already present), then verify indices[i]->dtype.code == kDLFloat &&
indices[i]->dtype.bits == 32 for every i, and validate that queries[i],
neighbors[i], and distances[i] are device-backed DLPack tensors with the
expected element types (float for queries/distances, uint32 for neighbors)
before calling reinterpret_cast<const IndexT*>(indices[i]->addr) and
cuvs::core::from_dlpack to populate q_vec[i], n_vec[i], d_vec[i]; replace the
blind casts with RAFT_EXPECTS that include i in the error messages so a bad
segment fails fast and clearly.
- Around line 726-736: The loop that builds idx_vec/q_vec/n_vec/d_vec must also
validate that all segment indices use the same distance metric as the first
segment to prevent mixing incompatible metrics; inside the for-loop in cagra.cpp
(after the existing RAFT_EXPECTS that checks indices[i] non-null) compare
indices[i]->metric (or the actual metric field name on your IndexT struct/class)
to indices[0]->metric and fail fast with RAFT_EXPECTS (or equivalent) and a
clear message like "Mixed distance metrics across segments: expected %s but got
%s at segment %u"; keep this check before pushing idx_vec[i] and before calling
cuvs::neighbors::cagra::search_multi_segment so the function only runs when all
segments share the same metric.

In `@c/src/selection/select_k.cpp`:
- Around line 14-40: cuvsSelectK dereferences shape[1] and casts buffers without
validating the DLPack tensors; add explicit validation at the top of cuvsSelectK
for in_val, out_val, out_idx (non-null), then check each
DLManagedTensor->dl_tensor for expected ndim (==2), shapes (rows match expected
1 or compatible), dtype (in_val/out_val float32, out_idx int64), device type
(CUDA) and device id, byte_offset == 0, and contiguous row-major
strides/compatibility before creating device views with
raft::make_device_matrix_view; if any check fails return an appropriate
cuvsError_t (or throw inside translate_exceptions) instead of proceeding to
casts and calling cuvs::selection::select_k so malformed callers cannot crash or
corrupt memory.

In `@cpp/include/cuvs/neighbors/cagra.hpp`:
- Around line 1752-1806: Add Doxygen documentation for each overloaded
search_multi_segment declaration so they appear in generated API docs; either
add brief doxygen blocks above each overload or use `@copydoc` to reference the
primary search_multi_segment doc block (e.g., use `@copydoc`
search_multi_segment(raft::resources const&,
cuvs::neighbors::cagra::search_params const&, const std::vector<const
cuvs::neighbors::cagra::index<float, uint32_t>*>&, const
std::vector<raft::device_matrix_view<const float, int64_t, raft::row_major>>&,
const std::vector<raft::device_matrix_view<int64_t, int64_t, raft::row_major>>&,
const std::vector<raft::device_matrix_view<float, int64_t, raft::row_major>>&))
for the overloads with half/int8_t/uint8_t and uint32_t/int64_t neighbor types
so each signature (the overloads of search_multi_segment) is documented.
- Around line 1744-1806: The header declares the template parameter order as <T,
OutputIdxT, IdxT> but the instantiation macro (used with <T, IdxT, OutputIdxT>
e.g. (data_t, uint32_t, int64_t)) expects <T, IdxT, OutputIdxT>, causing the
IdxT/OutputIdxT swap that trips the static_assert in cagra_search.cuh:276 and
breaks link-time overloads for int64_t; fix by changing the template parameter
order in the search_multi_segment declarations to <T, IdxT, OutputIdxT> (or
alternatively update the instantiation macro to match the declared order) so
types map correctly, and add missing Doxygen for overloads 2–8 by inserting a
`@copydoc` search_multi_segment (or equivalent documentation block) above each of
those overloaded search_multi_segment declarations to satisfy the public API
docs requirement.

In `@cpp/src/neighbors/cagra.cuh`:
- Around line 409-420: The wrapper search_multi_segment currently forwards
indices, queries, neighbors, and distances without validation; add the same
upfront shape checks used by search() before calling
cagra::detail::search_multi_segment: verify indices.size() == queries.size() ==
neighbors.size() == distances.size(), then for each segment i ensure
queries[i].n_rows == neighbors[i].n_rows, neighbors[i].n_cols ==
distances[i].n_cols (k matches), queries[i].n_cols equals the index dimension
for indices[i] (or indices[i]->dim() / appropriate accessor), and that k > 0 and
dims are consistent; if any check fails, return/throw a clear error (or use
RAFT/CUASSERT used elsewhere) rather than forwarding to the detail
implementation.

In `@java/cuvs-java/src/main/java/com/nvidia/cuvs/CuVSResources.java`:
- Around line 60-76: The Java API should validate workspace pool sizes before
calling native code: update the Javadoc for setWorkspacePool to state the valid
range is > 0, and in the implementation of setWorkspacePool (the method that
currently invokes the native cuvsResourcesSetWorkspacePool) add a check that
rejects non-positive values (<= 0) by throwing an appropriate Java exception
(e.g., IllegalArgumentException) with a clear message; only call
cuvsResourcesSetWorkspacePool when the value is positive to avoid
signed->unsigned wraparound in native size_t.

In `@java/cuvs-java/src/main/java/com/nvidia/cuvs/MultiSegmentSearchResults.java`:
- Around line 24-27: MultiSegmentSearchResults currently stores native uint32
ordinals in an int[] (field ordinals) which corrupts values > Integer.MAX_VALUE;
change ordinals from int[] to long[] (and any constructor/getter signatures) and
ensure code that decodes native uint32_t values writes unsigned values into the
long (e.g., value & 0xFFFFFFFFL) so ordinals remain non-negative; update any
consumers (notably MultiSegmentCagraSearch) to compare against a long sentinel
(e.g., -1L) or otherwise handle long ordinals instead of treating negative ints
as sentinels.

In `@java/cuvs-java/src/main/java/com/nvidia/cuvs/SynchronizedCuVSResources.java`:
- Around line 43-46: The setWorkspacePool method in SynchronizedCuVSResources is
not using the shared lock and must be serialized like access(); modify
setWorkspacePool to acquire the same lock used by access() (e.g., wrap the call
to inner.setWorkspacePool(sizeBytes) in the synchronized block or lock guard
used by access()) so that mutations to workspace pool are protected by the same
synchronization as access().

In `@java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CagraIndexImpl.java`:
- Around line 391-396: The offset calculation uses segmentIdx * topK
(neighborByteOffset/distanceByteOffset) but the code builds tensors with shape
{numQueries, topK} (numQueries from queryVectors.size()), so when numQueries > 1
the buffer offsets are wrong; either enforce single-query by adding a guard (if
(numQueries != 1) throw new IllegalArgumentException(...)) near where numQueries
is computed (queryVectors) or update the offsets to multiply by numQueries
(neighborByteOffset = segmentIdx * numQueries * topK * C_INT_BYTE_SIZE and
distanceByteOffset = segmentIdx * numQueries * topK * Float.BYTES) before
creating neighborSlice/distanceSlice (globalNeighborsDP/globalDistancesDP).

In `@java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CudaStreamPool.java`:
- Around line 115-119: Make nextSlot atomic and wrap the returned slot index by
changing slotCounter to an AtomicInteger and using an atomic get-and-add plus
modulo; specifically, replace the non-atomic increment in nextSlot with
something like int start = slotCounter.getAndAdd(count); then return
Math.floorMod(start, poolSize) (or equivalent using your pool size field) so the
operation is thread-safe and the returned slot is bounded by the pool size.
- Around line 125-131: In CudaStreamPool.close(), wrap the cuvsResourcesDestroy
call with the same error validation used elsewhere by replacing the raw
cuvsResourcesDestroy(resources[i]) invocation with a call to
checkCuVSError(cuvsResourcesDestroy(resources[i]), "cuvsResourcesDestroy") so
cleanup failures are logged/handled; keep the surrounding loop and existing
calls to checkCudaError(cudaEventDestroy(events[i]), "cudaEventDestroy") and
checkCudaError(cudaStreamDestroy(streams[i]), "cudaStreamDestroy") unchanged and
reference the close(), cuvsResourcesDestroy, checkCuVSError, events, resources,
streams, and size symbols to locate the change.
- Around line 59-85: The constructor CudaStreamPool currently leaks native
handles if a create call fails mid-loop; modify the CudaStreamPool(int size)
constructor to perform rollback cleanup on failure by tracking the current index
and, if any checkCudaError/checkCuVSError throws, iterating over
already-initialized entries in resources[], streams[], and events[] to call the
corresponding destroy functions (cudaStreamDestroy for streams[],
cudaEventDestroy for events[], and the cuvs resources destroy routine for
resources[]) before rethrowing the exception; implement this by wrapping the
allocation loop in try/catch (or try/finally with a success flag) and invoking
the same cleanup logic as close() for indices < currentIndex so no native
handles are leaked if construction aborts.

In
`@java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CuVSResourcesImpl.java`:
- Around line 31-33: The CudaStreamPool is currently allocated in the field
initializer (streamPool) which can leak native resources if
CuVSResourcesImpl(Path) throws before construction completes; move creation of
the CudaStreamPool from the field initializer into the CuVSResourcesImpl(Path)
constructor (use Integer.getInteger(CudaStreamPool.SIZE_PROPERTY,
CudaStreamPool.DEFAULT_SIZE) to determine size), assign it to the streamPool
field there, and in the constructor failure path ensure you call
streamPool.close() (or otherwise tear it down) before rethrowing so native
resources are not leaked; keep the streamPool field declaration but initialize
it only in the constructor and ensure close() is reachable on exceptions.

In `@java/cuvs-java/src/main/java22/com/nvidia/cuvs/MultiSegmentCagraSearch.java`:
- Around line 119-145: The code must enforce the single-query contract or size
outputs from the actual query row count: in MultiSegmentCagraSearch, after
obtaining var queryVectors = (CuVSMatrixInternal)
queries.get(i).getQueryVectors(), read its row count (e.g.,
queryVectors.getRowCount()/numRows()/size(0) — use the actual accessor on
CuVSMatrixInternal) into int nq; if nq > 1 throw an IllegalArgumentException
rejecting multi-row queries, or alternatively set segShape = new long[] {nq, k}
and compute neighbor/distance byte offsets and tensor sizes using nq*k (update
nByteOffset/dByteOffset and prepareTensor calls for
neighborsArray/distancesArray accordingly) so prepareTensor and the
globalNeighborsDP/globalDistancesDP slices match the query row count.
- Around line 96-111: The code in MultiSegmentCagraSearch currently uses
CagraSearchParams built from queries.get(0) (via
CuVSParamsHelper.buildCagraSearchParams) and thus drops per-segment settings
from subsequent CagraQuery entries; fix by either validating that all CagraQuery
instances in queries have identical search params and no per-segment filters
(throw IllegalArgumentException from MultiSegmentCagraSearch if any CagraQuery
differs from queries.get(0)), or plumb per-segment parameters through the native
call: extend the native wrapper (the cuvsCagraSearchMultiSegment binding) and
CuVSParamsHelper to accept/allocate an array of CagraSearchParams (build one
MemorySegment per CagraQuery) and pass that array/handle when invoking the
multi-segment search so each segment’s filters/params are applied.

---

Outside diff comments:
In `@java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraSearchParams.java`:
- Around line 293-322: The toString() in class CagraSearchParams currently omits
the new fields persistent, persistentLifetime, and persistentDeviceUsage; update
the CagraSearchParams.toString() method to append these three fields (with their
names and values) into the returned string (same formatting style as the other
fields) so debugging output shows the full configuration.

---

Nitpick comments:
In `@cpp/src/neighbors/detail/cagra/cagra_search.cuh`:
- Around line 388-417: Hoist the allocation and computation of query_norms out
of the segment loop and reuse it for every segment whose indices[i]->metric() ==
cuvs::distance::DistanceType::CosineExpanded: allocate query_norms once (using
raft::make_device_vector) before the for-loop, run raft::linalg::reduce on the
shared queries data to compute the norms, then inside the loop call
raft::linalg::matrix_vector_op (as currently done) using the precomputed
query_norms for each CosineExpanded segment; after the loop free or let
query_norms go out of scope. Ensure you still call
cuvs::neighbors::ivf::detail::postprocess_distances for non-CosineExpanded
branches and keep all existing ops (raft::compose_op, raft::sq_op,
raft::div_const_op, raft::cast_op, raft::add_const_op, raft::div_checkzero_op)
unchanged.

In `@java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraSearchParams.java`:
- Around line 527-537: The builder setter withPersistentDeviceUsage currently
assigns persistentDeviceUsage without enforcing the Javadoc constraint; update
Builder.withPersistentDeviceUsage to validate that persistentDeviceUsage > 0.0f
and <= 1.0f and throw an IllegalArgumentException (including the invalid value
in the message) when the check fails so invalid values are rejected early before
reaching native code.

In `@java/cuvs-java/src/test/java/com/nvidia/cuvs/CheckedCuVSResources.java`:
- Around line 59-62: The new delegate method setWorkspacePool(long sizeBytes) is
missing the wrapper's destroyed-state guard; modify
CheckedCuVSResources.setWorkspacePool to call checkNotDestroyed() at the start
(before delegating to inner.setWorkspacePool(sizeBytes)) so it matches the
class's defensive contract and uses the same destroyed-state check as other
methods.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 626e7fa1-95fc-4f40-b30d-d7fbcb6521a6

📥 Commits

Reviewing files that changed from the base of the PR and between f2bffb6 and 49e5a14.

📒 Files selected for processing (40)
  • c/CMakeLists.txt
  • c/include/cuvs/core/c_api.h
  • c/include/cuvs/neighbors/cagra.h
  • c/include/cuvs/selection/select_k.h
  • c/src/core/c_api.cpp
  • c/src/neighbors/cagra.cpp
  • c/src/selection/select_k.cpp
  • cpp/include/cuvs/neighbors/cagra.hpp
  • cpp/src/neighbors/cagra.cuh
  • cpp/src/neighbors/cagra_search_inst.cu.in
  • cpp/src/neighbors/detail/cagra/cagra_search.cuh
  • cpp/src/neighbors/detail/cagra/search_single_cta.cuh
  • cpp/src/neighbors/detail/cagra/search_single_cta_inst.cuh
  • cpp/src/neighbors/detail/cagra/search_single_cta_kernel-inl.cuh
  • cpp/src/neighbors/detail/cagra/search_single_cta_kernel.cuh
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraIndex.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/CagraSearchParams.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/CuVSAceParams.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/CuVSMatrix.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/CuVSResources.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswAceParams.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndex.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/HnswIndexParams.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/MultiSegmentSearchResults.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/SynchronizedCuVSResources.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/CuVSProvider.java
  • java/cuvs-java/src/main/java/com/nvidia/cuvs/spi/UnsupportedProvider.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/MultiSegmentCagraSearch.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/BufferedCagraSearch.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CagraIndexImpl.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CuVSMatrixBaseImpl.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CuVSMatrixInternal.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CuVSParamsHelper.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CuVSResourcesImpl.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CudaStreamPool.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/HnswIndexImpl.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/SelectKHelper.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/common/LinkerHelper.java
  • java/cuvs-java/src/main/java22/com/nvidia/cuvs/spi/JDKProvider.java
  • java/cuvs-java/src/test/java/com/nvidia/cuvs/CheckedCuVSResources.java

Comment thread c/include/cuvs/core/c_api.h Outdated
Comment thread c/src/neighbors/cagra.cpp Outdated
Comment thread c/src/neighbors/cagra.cpp Outdated
Comment thread c/src/selection/select_k.cpp
Comment thread cpp/include/cuvs/neighbors/cagra.hpp Outdated
Comment thread java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CudaStreamPool.java Outdated
Comment thread java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CudaStreamPool.java Outdated
Comment thread java/cuvs-java/src/main/java22/com/nvidia/cuvs/internal/CuVSResourcesImpl.java Outdated
Comment thread java/cuvs-java/src/main/java22/com/nvidia/cuvs/MultiSegmentCagraSearch.java Outdated
Comment thread java/cuvs-java/src/main/java22/com/nvidia/cuvs/MultiSegmentCagraSearch.java Outdated
@jamxia155

jamxia155 commented Apr 30, 2026

Copy link
Copy Markdown
Contributor Author

Pareto frontiers from initial benchmarking:
image
image

Hardware: 32/64-core/thread CPU, L40S 48GB
Dataset: 50M x 64 dim x 16-bit
Top-K: 100
CAGRA_HNSW (CAGRA index converted to HNSW, search on CPU) parameter sweep ranges:

  • "efSearch": [100, 150, 200]
  • "queryThreads": [1, 8, 64]
  • "cagraGraphDegree": [16, 32, 64, 128]
  • "cagraIntermediateGraphDegree": [32, 64, 128, 256, 512]
  • "cagraHnswLayers": [1, 2, 4, 6]

CAGRA_SEARCH (CAGRA index used directly for search on GPU) parameter sweep ranges:

  • "efSearch": [100, 150]
  • "queryThreads": [1, 8, 64]
  • "cagraGraphDegree": [16, 32, 64, 128]
  • "cagraIntermediateGraphDegree": [32, 64, 128, 256, 512]
  • "cagraSearchWidth": [4, 16, 128, 256, 1024]

@aamijar aamijar added non-breaking Introduces a non-breaking change improvement Improves an existing functionality labels May 1, 2026
@aamijar aamijar moved this to In Progress in Unstructured Data Processing May 1, 2026
@cjnolet cjnolet moved this to In Progress in Unstructured Data Processing May 10, 2026
Comment thread c/include/cuvs/neighbors/cagra.h Outdated
* @param[out] neighbors array of num_segments DLManagedTensor* (device, uint32, [nq, topk])
* @param[out] distances array of num_segments DLManagedTensor* (device, float32, [nq, topk])
*/
cuvsError_t cuvsCagraSearchMultiSegment(cuvsResources_t res,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to align on nomenclature a bit, I wonder if we can think of a more general name. Maybe "Partition"? Segment is pretty closely coupled to databases, and more specifically to LSM-based databases, but cuVS the library is more general that that. cuVS is at the level of "hash partitioning" or "blind sharding" (those are the terms we tend to use in this context). I think "MultiPartition" would be a more fitting name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aligning to "partition" for now. FYI, also considered: "MultiShard", "MultiIndex", "Federated", but these might come with unintended connotations.

Comment thread c/include/cuvs/selection/select_k.h Outdated
* @param[out] out_idx DLManagedTensor* shape [1, k], int64, device memory
* @return cuvsError_t
*/
cuvsError_t cuvsSelectK(cuvsResources_t res,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh this is great. I was just working on code examples for the new docs and realized we only have a C++ API for select_k. It'll be great to get the C APis, and later on the Python and other language wrappers for select-k.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the refactoring prompted by your other comment, select-k is no longer needed for this work. Leaving the C API intact in case it might be useful to others.

jamxia155 added 2 commits July 2, 2026 17:08
These tests search a 4-vector dataset with the default search params. The
default CAGRA search algo (AUTO) resolves to MULTI_CTA when max_queries is
small relative to the SM count (the common case), and MULTI_CTA fails to
settle all neighbors on such a pathologically small dataset, returning
invalid (-1 / FLT_MAX) entries in the top-k and breaking the exact-match
assertions.

AUTO is the correct default for real workloads, so leave the library
default unchanged and instead pin algo=SINGLE_CTA in the tests that rely
on the tiny hand-crafted dataset. Larger/randomized-dataset tests keep
AUTO. CagraAceBuildAndSearchIT and TieredIndexIT use the same small data
but build graphs that MULTI_CTA handles correctly, so they are left on
AUTO.
@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test 643bd3f

@anaruse

anaruse commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

@jamxia155, this is a great extension. Multi-partition CAGRA search is a useful capability, and the implementation work here is substantial.

Currently the single-index and multi-partition search paths are exposed as separate APIs: callers pass either a single cagra::index or a std::vector<const cagra::index*>, and must choose which path to use. That also means num_partitions == 1 through the multi-partition API follows a different execution path from the regular single-index API.

Is the plan to keep this public API shape long term, or is this intended as an interim step until a multi-partition/partitioned index abstraction is introduced? For example, an index object carrying partition metadata could let search dispatch internally based on the index representation and avoid making callers choose between APIs.

If the multi-partition API remains separate, should num_partitions == 1 be treated explicitly? For example, it could either be rejected with a clear error, or at least warn/document that this path is not equivalent to the regular single-index search path and may have different overhead.


if (params.max_queries == 0) {
cudaDeviceProp deviceProp = raft::resource::get_device_properties(res);
params.max_queries =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regular single-index path chunks the query set by max_queries before invoking the search plan. The multi-partition path appears to use the full query count directly for workspace allocation and kernel launch. Is that intentional? Should max_queries also bound the per-launch query count here, given that some workspaces scale with num_queries * num_partitions?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was an oversight. Added the chunking mechanism to align with existing practice. Thanks.

// Find the max graph_degree across all partitions (needed for the shared kernel plan).
int64_t max_graph_degree = 0;
int64_t max_dataset_size = 0;
for (uint32_t i = 0; i < num_partitions; i++) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we validate that all partitions have the same metric here? The final select_k direction is chosen from indices[0]->metric(), so mixed metrics would not be ordered correctly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added validation for uniform distance metrics.

for (uint32_t i = 0; i < num_partitions; i++) {
RAFT_EXPECTS(!indices[i]->dataset_fd().has_value(),
"Disk-based datasets are not supported for multi-partition search");
max_graph_degree = std::max(max_graph_degree, indices[i]->graph().extent(1));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation appears to support different graph degrees across partitions by sizing the plan with max_graph_degree and passing each partition's actual graph_degree to the kernel. Do we have a test covering mixed graph degrees? The current multi-partition tests seem to build all partitions with the same graph degree.

Given that shared-memory/layout sizing is driven by the maximum graph degree, mixed graph degrees may also be suboptimal for performance. If this is not a required use case, should we reject mixed graph degrees explicitly and document that all partitions should use the same degree?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added validation for uniform graph degree. Also added a note to the public header stating the requirements:

All index partitions must use the same distance metric and graph degree; partition sizes may differ.

@jamxia155

Copy link
Copy Markdown
Contributor Author

Currently the single-index and multi-partition search paths are exposed as separate APIs: callers pass either a single cagra::index or a std::vector<const cagra::index*>, and must choose which path to use. That also means num_partitions == 1 through the multi-partition API follows a different execution path from the regular single-index API.

Is the plan to keep this public API shape long term, or is this intended as an interim step until a multi-partition/partitioned index abstraction is introduced? For example, an index object carrying partition metadata could let search dispatch internally based on the index representation and avoid making callers choose between APIs.

If the multi-partition API remains separate, should num_partitions == 1 be treated explicitly? For example, it could either be rejected with a clear error, or at least warn/document that this path is not equivalent to the regular single-index search path and may have different overhead.

Hi @anaruse, thank you for your comments. There has been a discussion on API design here but it's probably been buried by all the updates since. The current consensus is to provide this feature quickly to satisfy customer needs while allowing ourselves some more time to think through the appropriate long-term abstraction to unify these search functions.

Re: num_partitions == 1, it is technically possible for the multi-partition execution path to subsume the single-partition one as a base case. For this PR, I deliberately tried (where possible) to keep the changes separate and additive so as to avoid regression in the pre-existing CAGRA search. As the multi-partition code path matures, it may become advisable to combine them. Currently, the initial user of this API is cuVS-Lucene and there is value in the simplicity of sticking to the multi-partition API even when num_partitions == 1 (e.g. single path of failure). To document the potentially different behavior in the single-partition case, I would like to go with a comment in the cuVS interface rather than a loud warning that a cuVS-Lucene end-user would see without knowing what to do about.

Will address your other comments and update.

jamxia155 added 3 commits July 3, 2026 14:23
…partition_bitset_filter

Multi-partition CAGRA search previously required callers to build a
`multi_partition_bitset_filter` carrying the concatenated bitset plus a
device array of per-partition bit offsets. Since the offsets are a pure
function of the per-partition index sizes, they can be recomputed inside
the search instead of transported across the API.

Core change:
- Delete `multi_partition_bitset_filter` and `FilterType::MultiPartitionBitset`.
  Multi-partition filtering now flows through the plain `bitset_filter`
  (the combined, 64-bit word-aligned bitset), matching the single-partition
  path and its JIT tag/fragment.
- Recompute each partition's starting bit offset host-side (word-aligned
  prefix sum of index sizes) in `search_multi_partition`, carry it in
  `multi_partition_desc_t::bit_offset`, and add it ONLY at the bitset filter
  test in `search_core` (via `bit_offset + to_source_index(...)`) — never to
  the output/traversal index. Guarded by a single size-consistency check.
- Delete the now-unused MP fragment path: `sample_filter_mp_bitset_impl`,
  `mp_bitset_filter_data_t`, `mp_cagra_bitset`, `extract_cagra_mp_sample_filter`,
  `is_mp_bitset_filter`, and `tag_filter_mp_bitset`.

C API:
- The multi-partition search now takes a `BITSET` filter (addr = the combined
  bitset DLPack tensor); the `MULTI_PARTITION_BITSET` enum value and
  `cuvsMultiPartitionBitsetFilter` struct are removed. Total bits and offsets
  are no longer passed.

cuvs-java:
- `FilterBitsetHandle.create` / `CuVSProvider.newFilterBitsetHandle` take only
  the combined bitset words; `MultiPartitionCagraSearchImpl` populates a BITSET
  `cuvsFilter`.
- Import the CUDA runtime symbols in `GPUInfoProviderImpl` from the leaf jextract
  binding class so they resolve regardless of how regeneration splits symbols.

Tests (C++, C, and the Java IT) updated to pack the combined bitset with
word-aligned per-partition slices. Validated across SINGLE_CTA/MULTI_CTA/AUTO,
filtered and unfiltered, all data types.
@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test 6b2b0b3

jamxia155 added 6 commits July 3, 2026 17:52
… C-API

cuvsCagraSearchMultiPartition previously hardcoded float32 indices and
uint32 neighbors, casting every partition to index<float, uint32_t> after
checking only indices[0]. It now dispatches at runtime on the index element
dtype (float32/float16/int8/uint8) and the neighbor output dtype
(uint32/int64) into a templated helper, matching the dispatch pattern and
dtype coverage of the single-index cuvsCagraSearch. The C ABI is unchanged;
dtypes continue to travel via the DLPack tensors. Every partition is now
validated to be non-null, built, and of the common index dtype before the
cast, and the header docs are corrected accordingly. Adds a multi-partition
int64-neighbor test for parity with single-partition coverage.
@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test 7a7c3e6

jamxia155 added 7 commits July 4, 2026 05:04
The decode loop treated any neighbor with the top bit set (neighbor < 0) as
the unfilled-slot sentinel, silently dropping valid uint32 ordinals in
[2^31, 2^32-2]. Match only the true 0xFFFFFFFF sentinel, and fail fast if a
valid ordinal exceeds Integer.MAX_VALUE rather than returning a negative
value that could cause out-of-bounds indexing downstream.
search_multi_partition passed the full query count to the kernels, whose
grid.y is bounded by maxGridSize[1], and sized the intermediate workspaces
to num_queries * num_partitions. Loop over max_queries-sized chunks (as the
single-index path does): keep grid.y within the limit, bound peak workspace
memory to one chunk, and offset the filter per chunk. Also guard the query-
norm reduction on CosineExpanded. Add a small-max_queries multi-partition
test to exercise the chunk loop, partial final chunk, and per-chunk filter
offset.
Multi-partition search derives the shared plan descriptor (and its
shared-memory/layout sizing) and the cross-partition select_k direction
from indices[0], so partitions with a differing metric or graph degree
would be searched or merged incorrectly. Validate both up front, document
the constraints in the public header, and add rejection tests. Dataset
sizes may still differ (e.g. skewed splits).
@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test 1770971

@jamxia155

Copy link
Copy Markdown
Contributor Author

/ok to test fa29064

@anaruse

anaruse commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Thanks @jamxia155.
The added max_queries chunking, the metric/graph-degree validation, and the API notes address my concerns.
LGTM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Improves an existing functionality non-breaking Introduces a non-breaking change

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

7 participants